#include "GribSyncFile.h"
#include "GribVariable.h"
#include "GribOutput.h"
#include "OCMain/Log.h"
#include <numeric>

#include "degrib/metaname.h"
#include "degrib/scan.h"
#include "degrib/myerror.h"
#include "degrib/clock.h"
#include "degrib/weather.h"
#include "degrib/hazard.h"
#include "degrib/inventory.h"
#include "degrib/grib2.h"
#include "degrib/gridtemplates.h"
#include "degrib/degrib1.h"

namespace OCPP
{
	using namespace Volume;
	class InventoryWrapperGribEx : public InventoryWrapper
	{
	public:
		explicit InventoryWrapperGribEx(void* fp) : InventoryWrapper()
		{
			result_ = GRIB2Inventory(fp, &inv_, &inv_len_, 0 /* all messages */,
				&num_messages_);
		}

		~InventoryWrapperGribEx() override
		{
			if (inv_ == nullptr)
				return;
			for (uInt4 i = 0; i < inv_len_; i++)
			{
				GRIB2InventoryFree(inv_ + i);
			}
			free(inv_);
		}
	};

	std::mutex GribSyncFile::intanceMutex;
	GribSyncFile::NcSyncFileInstances GribSyncFile::instances;

	GribSyncFile::GribSyncFile()
	{

	}

	GribSyncFile::~GribSyncFile()
	{

	}

	bool GribSyncFile::open(String fileName)
	{
		ncFile.open(fileName, std::ios::binary);
		return ncFile.is_open();
	}

	GribVariablePtr GribSyncFile::getVar(String name)
	{
		auto data = getVars();
		auto it = data.find(name);
		if (it != data.end())
		{
			return it->second;
		}
		return nullptr;
	}

	VariableMap GribSyncFile::getVars()
	{
		CORE_LOCK_MUTEX(mutex);

		if (mVariableMap.empty())
		{
			InventoryWrapperGribEx inventory(&ncFile);

			std::map<String, time_t> foreMap;

			for (int i = 0; i < inventory.length(); i++)
			{
				inventoryType* invT = inventory.get(i);

				std::shared_ptr<InventoryEx> invEx = std::make_shared<InventoryEx>();

				String unitName = invT->unitName;

				String comment = StringUtil::replaceAll(invT->comment, unitName, "");
				StringUtil::trim(comment);
				StringUtil::trim(unitName, true, true, " []");

				invEx->foreSec = (time_t)invT->foreSec;
				invEx->validTime = (time_t)invT->validTime;
				invEx->refTime = (time_t)invT->refTime;

				invEx->unitName = unitName;
				invEx->start = invT->start;
				invEx->subgNum = invT->subgNum;
				invEx->GribVersion = invT->GribVersion;
				invEx->msgNum = invT->msgNum;
				invEx->shortFstLevel = invT->shortFstLevel;

				String longFstLevel = invT->longFstLevel;
				invEx->longFstLevel = longFstLevel;
				size_t f0 = longFstLevel.find("[");
				size_t f1 = longFstLevel.find("]");
				if (f0 != String::npos && f1 != String::npos)
				{
					String levelUnit = longFstLevel.substr(f0 + 1, f1 - f0 - 1);
					if (levelUnit != "-")
					{
						invEx->levelUnit = levelUnit;
					}
				}

				StringVector sv = StringUtil::split(invT->shortFstLevel, "-");
				if (sv.size() == 2)
				{
					invEx->level = StringConverter::parseReal(sv[0]);
					String name = sv[1];
					comment += "_" + name;
				}
				if (sv.size() == 3)
				{
					invEx->level = StringConverter::parseReal(sv[0]);
					invEx->level2 = StringConverter::parseReal(sv[1]);
					String name = sv[2];
					comment += "_" + name;
				}

				comment = StringUtil::replaceAll(comment, " ", "_");

				invEx->Grib2_Parameter = { invT->prodType, (int)invT->cat, (int)invT->subcat, invT->fstSurfType };

				comment = StringUtil::replaceAll(comment, "Mean_sea_level", "msl");
				comment = StringUtil::replaceAll(comment, "Entire", "entire");
				comment = StringUtil::replaceAll(comment, "Atmosphere", "atmosphere");
				comment = StringUtil::replaceAll(comment, "Ground_or_water_surface", "surface");
				comment = StringUtil::replaceAll(comment, "SFC", "surface");
				comment = StringUtil::replaceAll(comment, "Specified_height_level_above_ground", "height_above_ground");
				comment = StringUtil::replaceAll(comment, "Isobaric_surface", "isobaric");
				comment = StringUtil::replaceAll(comment, "ISBL", "isobaric");
				comment = StringUtil::replaceAll(comment, "(", "");
				comment = StringUtil::replaceAll(comment, ")", "");

				String statisticl;

				switch (invT->staProcess)
				{
				case GS4_STATIC_Average:
					statisticl = "Average";
					break;
				case GS4_STATIC_Accumulation:
					statisticl = "Accumulation";
					break;
				case GS4_STATIC_Maximum:
					statisticl = "Maximum";
					break;
				case GS4_STATIC_Minimum:
					statisticl = "Minimum";
					break;
				case GS4_STATIC_RootMeanSquare:
					statisticl = "RootMeanSquare";
					break;
				case GS4_STATIC_StandardDeviation:
					statisticl = "StandardDeviation";
					break;
				case GS4_STATIC_Covariance:
					statisticl = "Covariance";
					break;
				case GS4_STATIC_Difference:
					statisticl = "Difference";
					break;
				case GS4_STATIC_Ratio:
					statisticl = "Ratio";
					break;
				case GS4_STATIC_StandardizedAnomaly:
					statisticl = "StandardizedAnomaly";
					break;
				case GS4_STATIC_Summation:
					statisticl = "Summation";
					break;
				case GS4_STATIC_Returnperiod:
					statisticl = "Returnperiod";
					break;
				default:
					break;
				}

				if (!statisticl.empty())
				{
					if (invT->lenTime > 0)
					{
						comment = str_format("%s_%d_Minute", comment.c_str(), invT->lenTime * 60);
					}
					comment += "_" + statisticl;
				}

				if (invT->templat == GS4_ENSEMBLE || invT->templat == GS4_ENSEMBLE_STAT)
				{
					comment += "_ens";
				}

				invEx->comment = comment;

				if (!mVariableMap[comment].get())
				{
					mVariableMap[comment] = std::make_shared<GribVariable>();
				}

				mVariableMap[comment]->insert(invEx);
			}
		}

		return mVariableMap;
	}

	VarData GribSyncFile::getData(GribVariable* variable, GridShape& origin,GridShape& shape, const VolumeOptions& options)
	{
		VarData varData;

		int timeIndex = origin[0];
		int originLevel = origin[1];
		int shapeLevel = shape[1];

		const auto& levelVec = variable->getLevelVecByTime(timeIndex);
		int numLevel = (int)levelVec.size();

		InventoryVector inVec;

		for (auto i = 0; i < shapeLevel; i++)
		{
			const auto& inv = variable->getInventor(i + originLevel);
			inVec.push_back(inv);

			VarData varData1 = getData(inv.get());
			auto nx = varData1.Nx;
			auto ny = varData1.Ny;

			if (varData.dv.empty())
			{
				variable->afterSetDimXY(varData1);
				auto size = shape[0] * shape[1];
				varData.Nx = varData1.Nx;
				varData.Ny = varData1.Ny;
				varData.lon1 = varData1.lon1;
				varData.lon2 = varData1.lon2;
				varData.lat1 = varData1.lat1;
				varData.lat2 = varData1.lat2;
				varData.dv.resize(size * nx * ny);
			}

			auto offset = i * nx * ny;
			auto faceSize = varData1.dv.size();
			memcpy(&varData.dv[offset], &varData1.dv[0], sizeof(double) * faceSize);
		}

		return varData;
	}

	GribOutput GribSyncFile::getData(std::shared_ptr<GribVariable> variable, GribTime time, int levelIndex, const VolumeOptions& options)
	{
		GribOutput gribOutput;
		gribOutput.addDimension(time.toString());

		const auto& levelVec = variable->getLevelVec(time);
		int numLevel = (int)levelVec.size();

		InventoryVector inVec;

		if (levelIndex > -1 && levelIndex < numLevel)
		{
			auto& [level, idx] = levelVec[levelIndex];
			const auto& inv = variable->getInventor(idx);
			inVec.push_back(inv);
			String levelStr = level.toString() + inv->levelUnit;
			gribOutput.addDimension(levelStr);
		}
		else
		{
			for (auto& [level, idx] : levelVec)
			{
				const auto& inv = variable->getInventor(idx);
				inVec.push_back(inv);
			}
		}

		bool unitPa = StringUtil::contains(variable->getName(), "isobaric", false);
		//bool reverseLevel = inVec.size() > 1 && unitPa;

		//if (options.reverseZ)
		//{
			std::sort(inVec.begin(), inVec.end(),
				[](const std::shared_ptr<InventoryEx>& x, const std::shared_ptr<InventoryEx>& y)
				{
					return x->level > y->level;
				}
			);
		//}

		int minRangeLevel = -10000;
		int maxRangeLevel = 100000;
		if (options.clipLevelRange.has_value())
		{
			Vector2i range = options.clipLevelRange.value();
			minRangeLevel = std::min(range[0], range[1]);
			maxRangeLevel = std::max(range[0], range[1]);
		}

		for (auto i = 0;i<inVec.size();i++)
		{
			if (i < minRangeLevel || i > maxRangeLevel) continue;
			auto inv = inVec[i];
			VarData varData1 = getData(inv.get());
			gribOutput.insert(varData1, inv->level);
		}

		return gribOutput;
	}

	VarData GribSyncFile::getData(InventoryEx* inv)
	{
		CORE_LOCK_MUTEX(mutex);

		VarData varData;

		ncFile.clear();
		ncFile.seekg(inv->start, std::ios::beg);

		IS_dataType is;
		IS_Init(&is);
		grib_MetaData meta;
		MetaInit(&meta);

		uInt4 grib_DataLen = 0;
		double* grib_Data = NULL;

		double majEarth = 0.0;  // -radEarth if < 6000 ignore, otherwise use this
		// to override the radEarth in the GRIB1 or GRIB2
		// message.  Needed because NCEP uses 6371.2 but
		// GRIB1 could only state 6367.47.
		double minEarth = 0.0;  // -minEarth if < 6000 ignore, otherwise use this
		// to override the minEarth in the GRIB1 or GRIB2

		const int simpWWA = 0;  // seem to be unused in degrib
		sChar f_unit = 0;
		sInt4 f_endMsg = 1;  // 1 if we read the last grid in a GRIB message, or we
		sChar f_SimpleVer = 4;  // Which version of the simple NDFD Weather table
		// to use. (1 is 6/2003) (2 is 1/2004) (3 is
		// 2/2004) (4 is 11/2004) (default 4)
		LatLon lwlf;            // Lower left corner (cookie slicing) -lwlf
		LatLon uprt;            // Upper right corner (cookie slicing) -uprt

		ReadGrib2Record(&ncFile, f_unit, &grib_Data, &grib_DataLen, &meta, &is, inv->subgNum,
			majEarth, minEarth, f_SimpleVer, simpWWA, &f_endMsg, &lwlf,
			&uprt);

		if (grib_Data)
		{
			varData.lon1 = meta.gds.lon1;
			varData.lat1 = meta.gds.lat1;
			varData.lon2 = meta.gds.lon2;
			varData.lat2 = meta.gds.lat2;
			varData.Nx = meta.gds.Nx;
			varData.Ny = meta.gds.Ny;

			varData.dv.resize(grib_DataLen);
			memcpy(&varData.dv[0], grib_Data, sizeof(double) * grib_DataLen);
			delete[] grib_Data;
		}

		IS_Free(&is);
		MetaFree(&meta);

		return varData;
	}
}